/* Command.cpp
Michael Zahniser, 2008

Function definitions for the Command class.
*/

#include "Command.h"

#include <algorithm>

using std::equal;



// Parse a line of text into a command.
Command::Command(const string &line)
{
    if(ParseQuit(line))
	return;
    if(ParseBalance(line))
	return;
    if(ParseChecks(line))
	return;
    if(ParseInfo(line))
	return;
    if(ParseRecords(line))
	return;
    if(ParseTransact(line))
	return;
    if(ParseCancel(line))
	return;
    if(ParseSetInfo(line))
	return;
    if(ParseMake(line))
	return;
    type = NONE;
}



// Get the type of this command.
Command::Type Command::GetType() const
{
    return type;
}



// Get the date specified in this command, if any.
const Date &Command::GetDate() const
{
    return date;
}



// Get the account name specified in this command, if any.
const string &Command::GetName() const
{
    return name;
}



// Check if the name specified in this command matches the given name.
bool Command::MatchesName(const string &accountName) const
{
    return HasPrefix(accountName, name);
}



// Get any additional argument that was specified.
const string &Command::GetArgument() const
{
    return argument;
}



// Get the account types included by this command's filter.
const vector<AccountType> &Command::GetAccountTypes() const
{
    return accountTypes;
}



bool Command::ParseQuit(const string &line)
{
    if(!HasPrefix(line, "quit") && !HasPrefix(line, "exit"))
	return false;

    type = QUIT;
    return true;
}



bool Command::ParseBalance(const string &line)
{
    // This flag stores whether this is a budget query. A budget query shows
    // changes in balances during the given time. A non-budget query shows
    // the total balances at a given time.
    bool isBudgetQuery = true;
    if(HasPrefix(line, "inc"))
    {
	accountTypes.push_back(AccountType::IN);
    }
    else if(HasPrefix(line, "out"))
    {
	accountTypes.push_back(AccountType::OUT);
    }
    else if(HasPrefix(line, "bud"))
    {
	accountTypes.push_back(AccountType::IN);
	accountTypes.push_back(AccountType::OUT);
    }
    else if(HasPrefix(line, "acc"))
    {
	accountTypes.push_back(AccountType::ACCOUNT);
	isBudgetQuery = false;
    }
    else if(HasPrefix(line, "all"))
    {
	accountTypes.push_back(AccountType::IN);
	accountTypes.push_back(AccountType::OUT);
	accountTypes.push_back(AccountType::ACCOUNT);
	accountTypes.push_back(AccountType::VIRTUAL);
	isBudgetQuery = false;
    }
    else
	return false;

    // Skip whitespace.
    string::const_iterator it = line.begin();
    NextToken(it, line);

    ParseDate(it, line);

    if(!isBudgetQuery)
    {
	// If this is a balances query (as opposed to a budget query)
	// the date should be set to the end of the range covered by the
	// date that was read in above.
	if(date.Year() == 0xFFFF)
	    date.SetYear(0xFFFE);
	if(date.Month() == 0xFF)
	    date.SetMonth(0xFE);
	date.SetDay(0xFE);
    }

    // We are now at the beginning of the first token that does not match one
    // of the special cases above. Everything from here on is the name.
    name.assign(it, line.end());
    type = BALANCE;
    return true;
}



bool Command::ParseChecks(const string &line)
{
    if(!HasPrefix(line, "che") && !HasPrefix(line, "pen"))
	return false;

    type = CHECKS;
    return true;
}



bool Command::ParseInfo(const string &line)
{
    if(!HasPrefix(line, "info"))
	return false;
    
    string::const_iterator it = line.begin();
    NextToken(it, line);
    name.assign(it, line.end());
    
    type = INFO;
    return true;
}



bool Command::ParseRecords(const string &line)
{
    if(!HasPrefix(line, "rec"))
	return false;
    
    string::const_iterator it = line.begin();
    NextToken(it, line);

    // Read the specified date, if any.
    ParseDate(it, line);

    // The rest of the line is the name to filter by.
    name.assign(it, line.end());
    
    type = RECORDS;
    return true;
}



bool Command::ParseTransact(const string &line)
{
    if(!HasPrefix(line, "tr"))
	return false;

    type = TRANSACT;
    return true;
}



bool Command::ParseCancel(const string &line)
{
    if(!HasPrefix(line, "x"))
	return false;

    type = CANCEL;
    return true;
}



bool Command::ParseSetInfo(const string &line)
{
    if(HasPrefix(line, "set"))
	type = SET;
    else if(HasPrefix(line, "clear"))
	type = CLEAR;
    else if(HasPrefix(line, "type"))
	type = TYPE;
    else
	return false;

    string::const_iterator it = line.begin();
    NextToken(it, line);
    string::const_iterator last = it;
    while(last != line.end() && *last > ' ')
	++last;
    argument.assign(it, last);

    NextToken(it, line);
    name.assign(it, line.end());
    return true;
}



bool Command::ParseMake(const string &line)
{
    if(!HasPrefix(line, "make"))
	return false;

    string::const_iterator it = line.begin();
    NextToken(it, line);
    name.assign(it, line.end());
    
    type = MAKE;
    return true;
}



// Parse a line, loiking for a date specification.
void Command::ParseDate(string::const_iterator &it, const string &line)
{
    // Check for a month specifier.
    int month = 0;
    if(line.end() - it >= 3)
	for(int i = 0; i < 12; ++i)
	    if(HasPrefix(it, MONTH[i]))
		month = i + 1;

    // If a month was read, skip the name of the month.
    if(month)
	NextToken(it, line);

    // If the next token is a number, read it as a date.
    if(IsNumber(it, line))
    {
	string::const_iterator end = it;
	while(end != line.end() && *end > ' ')
	    ++end;

	date = string(it, end);
	NextToken(it, line);
    }
    else
    {
	date = Date(Date::Today().Year());
    }

    // Now, the date has been read, if specified. Update it using the month
    // that may have been read before.
    if(month)
	date.SetMonth(month);
}



bool Command::HasPrefix(const string &text, const string &prefix)
{
    return (text.length() >= prefix.length()) &&
	HasPrefix(text.begin(), prefix);
}



bool Command::HasPrefix(string::const_iterator it, const string &prefix)
{
    return equal(prefix.begin(), prefix.end(), it);
}



void Command::NextToken(string::const_iterator &it, const string &line)
{
    while(it != line.end() && *it > ' ')
	++it;
    while(it != line.end() && *it <= ' ')
	++it;    
}



bool Command::IsNumber(string::const_iterator it, const string &line)
{
    if(it == line.end())
	return false;
    
    for( ; it != line.end() && *it > ' '; ++it)
	if((*it < '0') | (*it > '9'))
	    return false;
    return true;
}



const string Command::MONTH[] = {
    "jan", "feb", "mar", "apr", "may", "jun",
    "jul", "aug", "sep", "oct", "nov", "dec"};
